import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.file.Files;
import java.nio.file.Path;
import sdl3.SDL_AudioSpec;
import sdl3.SDL_Event;
import sdl3.SDL_FColor;
import sdl3.SDL_GPUBufferBinding;
import sdl3.SDL_GPUBufferCreateInfo;
import sdl3.SDL_GPUBufferRegion;
import sdl3.SDL_GPUColorTargetDescription;
import sdl3.SDL_GPUColorTargetInfo;
import sdl3.SDL_GPUGraphicsPipelineCreateInfo;
import sdl3.SDL_GPUGraphicsPipelineTargetInfo;
import sdl3.SDL_GPUShaderCreateInfo;
import sdl3.SDL_GPUTransferBufferCreateInfo;
import sdl3.SDL_GPUTransferBufferLocation;
import sdl3.SDL_GPUVertexAttribute;
import sdl3.SDL_GPUVertexBufferDescription;
import sdl3.SDL_GPUVertexInputState;
import sdl3.SDL_KeyboardEvent;
import static java.lang.foreign.MemorySegment.NULL;
import static java.lang.foreign.ValueLayout.*;
import static sdl3.SDL_h.*;

// --enable-native-access=ALL-UNNAMED
// -XstartOnFirstThread for Mac
// https://github.com/KhronosGroup/glslang/releases for glslang tool
public final class Sdl3Demo {
	static void main() throws IOException {
		var inited = false;
		int audioDeviceId = 0;
		var audioStream = NULL;
		var window = NULL;
		var gpuDevice = NULL;
		var pipeline = NULL;
		var vertBuf = NULL;
//		var renderer = NULL;
		try (final var arena = Arena.ofConfined()) {
			SDL_SetAppMetadata(
					arena.allocateFrom("SDL3 Demo"),
					arena.allocateFrom("1.0"),
					arena.allocateFrom("com.example.sdl3-demo")
			);
			if (!SDL_Init(SDL_INIT_AUDIO() | SDL_INIT_VIDEO()))
				throw new RuntimeException("SDL_Init failed: " + SDL_GetError().getString(0));
			inited = true;

			for (int i = 0, n = SDL_GetNumAudioDrivers(); i < n; i++)
				System.out.format("AudioDriver-%d: %s\n", i, SDL_GetAudioDriver(i).getString(0));
			final var audioDriver = SDL_GetCurrentAudioDriver();
			System.out.println("CurrentAudioDriver: " + (audioDriver != null ? audioDriver.getString(0) : "<null>"));
			final var audioSpec = SDL_AudioSpec.allocate(arena);
			final var audioBufSizePtr = arena.allocate(JAVA_INT, 1);
			final var audioDeviceIds = SDL_GetAudioPlaybackDevices(NULL);
			if (audioDeviceIds != NULL) {
				for (int i = 0; ; i++) {
					final int devId = audioDeviceIds.getAtIndex(JAVA_INT, i);
					if (devId == 0)
						break;
					System.out.format("AudioDevice-%d: %d: %s", i, devId, SDL_GetAudioDeviceName(devId).getString(0));
					if (SDL_GetAudioDeviceFormat(devId, audioSpec, audioBufSizePtr)) {
						System.out.format(", channels=%d, format=%s, freq=%d, bufSamples=%d\n",
								SDL_AudioSpec.channels(audioSpec),
								SDL_GetAudioFormatName(SDL_AudioSpec.format(audioSpec)).getString(0),
								SDL_AudioSpec.freq(audioSpec), audioBufSizePtr.get(JAVA_INT, 0));
					} else
						System.out.println();
				}
				SDL_free(audioDeviceIds);
			}
			audioDeviceId = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK(), NULL);
			if (audioDeviceId == 0)
				throw new RuntimeException("SDL_OpenAudioDevice failed: " + SDL_GetError().getString(0));
			System.out.print("OpenLogicAudioDevice: " + audioDeviceId);
			if (SDL_GetAudioDeviceFormat(audioDeviceId, audioSpec, audioBufSizePtr)) {
				System.out.format(", channels=%d, format=%s, freq=%d, bufSamples=%d\n",
						SDL_AudioSpec.channels(audioSpec),
						SDL_GetAudioFormatName(SDL_AudioSpec.format(audioSpec)).getString(0),
						SDL_AudioSpec.freq(audioSpec), audioBufSizePtr.get(JAVA_INT, 0));
			} else
				System.out.println();
			SDL_AudioSpec.channels(audioSpec, 2);
			SDL_AudioSpec.format(audioSpec, SDL_AUDIO_S16LE()); // SDL_AUDIO_F32
			SDL_AudioSpec.freq(audioSpec, 48000);
			audioStream = SDL_CreateAudioStream(audioSpec, NULL);
			if (audioStream == NULL)
				throw new RuntimeException("SDL_CreateAudioStream failed: " + SDL_GetError().getString(0));
			if (!SDL_BindAudioStream(audioDeviceId, audioStream))
				throw new RuntimeException("SDL_BindAudioStream failed: " + SDL_GetError().getString(0));
			final var wavPath = Path.of("~$test.wav");
			if (Files.exists(wavPath)) {
				var heapData = Files.readAllBytes(wavPath);
				var offHeapData = Arena.global().allocate(heapData.length - 44);
				MemorySegment.copy(heapData, 44, offHeapData, JAVA_BYTE, 0, heapData.length - 44);
				SDL_PutAudioStreamDataNoCopy(audioStream, offHeapData, heapData.length - 44, NULL, NULL);
			}
			// SDL_ConvertAudioSamples(); SDL_FlushAudioStream(); SDL_SetAudioStreamGain(); SDL_SetAudioStreamFrequencyRatio();

			window = SDL_CreateWindow(arena.allocateFrom("Sdl3Demo"), 640, 480, SDL_WINDOW_RESIZABLE());
			if (window == NULL)
				throw new RuntimeException("SDL_CreateWindow failed: " + SDL_GetError().getString(0));

			var gpuDriver = NULL;
			for (int i = 0, n = SDL_GetNumGPUDrivers(); i < n; i++) {
				final var gd = SDL_GetGPUDriver(i);
				final var gds = gd.getString(0);
				System.out.println("GpuDriver-" + i + ": " + gds);
				if (gds.equals("vulkan")) // direct3d12
					gpuDriver = gd;
			}
			gpuDevice = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV(), false, gpuDriver); // SDL_GPU_SHADERFORMAT_DXIL() for direct3d12
			if (gpuDevice == NULL)
				throw new RuntimeException("SDL_CreateGPUDevice failed: " + SDL_GetError().getString(0));
			if (!SDL_ClaimWindowForGPUDevice(gpuDevice, window))
				throw new RuntimeException("SDL_ClaimWindowForGPUDevice failed: " + SDL_GetError().getString(0));
			//------
			final var codeSize = arena.allocate(JAVA_LONG, 1);
			var code = SDL_LoadFile(arena.allocateFrom("res/simple.vert.spv"), codeSize);
			if (code == NULL)
				throw new RuntimeException("SDL_LoadFile(vert.spv) failed: " + SDL_GetError().getString(0));
			final var shaderCreateInfo = SDL_GPUShaderCreateInfo.allocate(arena);
			SDL_GPUShaderCreateInfo.code_size(shaderCreateInfo, codeSize.get(JAVA_LONG, 0));
			SDL_GPUShaderCreateInfo.code(shaderCreateInfo, code);
			SDL_GPUShaderCreateInfo.entrypoint(shaderCreateInfo, arena.allocateFrom("main"));
			SDL_GPUShaderCreateInfo.format(shaderCreateInfo, SDL_GPU_SHADERFORMAT_SPIRV()); // SDL_GetGPUShaderFormats(gpuDevice);
			SDL_GPUShaderCreateInfo.stage(shaderCreateInfo, SDL_GPU_SHADERSTAGE_VERTEX());
			SDL_GPUShaderCreateInfo.num_samplers(shaderCreateInfo, 0);
			SDL_GPUShaderCreateInfo.num_storage_textures(shaderCreateInfo, 0);
			SDL_GPUShaderCreateInfo.num_storage_buffers(shaderCreateInfo, 0);
			SDL_GPUShaderCreateInfo.num_uniform_buffers(shaderCreateInfo, 0);
			final var vertShader = SDL_CreateGPUShader(gpuDevice, shaderCreateInfo);
			if (vertShader == NULL)
				throw new RuntimeException("SDL_CreateGPUShader(vertShader) failed: " + SDL_GetError().getString(0));
			SDL_free(code);
			code = SDL_LoadFile(arena.allocateFrom("res/simple.frag.spv"), codeSize);
			if (code == NULL)
				throw new RuntimeException("SDL_LoadFile(frag.spv) failed: " + SDL_GetError().getString(0));
			SDL_GPUShaderCreateInfo.code_size(shaderCreateInfo, codeSize.get(JAVA_LONG, 0));
			SDL_GPUShaderCreateInfo.code(shaderCreateInfo, code);
			SDL_GPUShaderCreateInfo.entrypoint(shaderCreateInfo, arena.allocateFrom("main"));
			SDL_GPUShaderCreateInfo.format(shaderCreateInfo, SDL_GPU_SHADERFORMAT_SPIRV()); // SDL_GetGPUShaderFormats(gpuDevice);
			SDL_GPUShaderCreateInfo.stage(shaderCreateInfo, SDL_GPU_SHADERSTAGE_FRAGMENT());
			SDL_GPUShaderCreateInfo.num_samplers(shaderCreateInfo, 0);
			SDL_GPUShaderCreateInfo.num_storage_textures(shaderCreateInfo, 0);
			SDL_GPUShaderCreateInfo.num_storage_buffers(shaderCreateInfo, 0);
			SDL_GPUShaderCreateInfo.num_uniform_buffers(shaderCreateInfo, 0);
			final var fragShader = SDL_CreateGPUShader(gpuDevice, shaderCreateInfo);
			if (fragShader == NULL)
				throw new RuntimeException("SDL_CreateGPUShader(fragShader) failed: " + SDL_GetError().getString(0));
			SDL_free(code);
			//------
			final var vertBufDesc = SDL_GPUVertexBufferDescription.allocate(arena);
			SDL_GPUVertexBufferDescription.slot(vertBufDesc, 0);
			SDL_GPUVertexBufferDescription.pitch(vertBufDesc, (int)(JAVA_FLOAT.byteSize() * 3));
			SDL_GPUVertexBufferDescription.input_rate(vertBufDesc, SDL_GPU_VERTEXINPUTRATE_VERTEX());
			SDL_GPUVertexBufferDescription.instance_step_rate(vertBufDesc, 0);
			final var vertAttr = SDL_GPUVertexAttribute.allocate(arena);
			SDL_GPUVertexAttribute.location(vertAttr, 0);
			SDL_GPUVertexAttribute.buffer_slot(vertAttr, 0);
			SDL_GPUVertexAttribute.format(vertAttr, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3());
			SDL_GPUVertexAttribute.offset(vertAttr, 0);
			final var vertInputState = SDL_GPUVertexInputState.allocate(arena);
			SDL_GPUVertexInputState.vertex_buffer_descriptions(vertInputState, vertBufDesc);
			SDL_GPUVertexInputState.num_vertex_buffers(vertInputState, 1);
			SDL_GPUVertexInputState.vertex_attributes(vertInputState, vertAttr);
			SDL_GPUVertexInputState.num_vertex_attributes(vertInputState, 1);
			final var colorTargetDesc = SDL_GPUColorTargetDescription.allocate(arena);
			SDL_GPUColorTargetDescription.format(colorTargetDesc, SDL_GetGPUSwapchainTextureFormat(gpuDevice, window));
			// SDL_GPUColorTargetDescription.blend_state(colorTargetDesc);
			final var pipelineTargetInfo = SDL_GPUGraphicsPipelineTargetInfo.allocate(arena);
			SDL_GPUGraphicsPipelineTargetInfo.color_target_descriptions(pipelineTargetInfo, colorTargetDesc);
			SDL_GPUGraphicsPipelineTargetInfo.num_color_targets(pipelineTargetInfo, 1);
			SDL_GPUGraphicsPipelineTargetInfo.depth_stencil_format(pipelineTargetInfo, 0);
			SDL_GPUGraphicsPipelineTargetInfo.has_depth_stencil_target(pipelineTargetInfo, false);
			final var pipelineCreateInfo = SDL_GPUGraphicsPipelineCreateInfo.allocate(arena);
			SDL_GPUGraphicsPipelineCreateInfo.vertex_shader(pipelineCreateInfo, vertShader);
			SDL_GPUGraphicsPipelineCreateInfo.fragment_shader(pipelineCreateInfo, fragShader);
			SDL_GPUGraphicsPipelineCreateInfo.vertex_input_state(pipelineCreateInfo, vertInputState);
			SDL_GPUGraphicsPipelineCreateInfo.primitive_type(pipelineCreateInfo, SDL_GPU_PRIMITIVETYPE_TRIANGLELIST());
			// SDL_GPUGraphicsPipelineCreateInfo.rasterizer_state(pipelineCreateInfo);
			// SDL_GPUGraphicsPipelineCreateInfo.multisample_state(pipelineCreateInfo);
			// SDL_GPUGraphicsPipelineCreateInfo.depth_stencil_state(pipelineCreateInfo);
			SDL_GPUGraphicsPipelineCreateInfo.target_info(pipelineCreateInfo, pipelineTargetInfo);
			pipeline = SDL_CreateGPUGraphicsPipeline(gpuDevice, pipelineCreateInfo);
			if (pipeline == NULL)
				throw new RuntimeException("SDL_CreateGPUGraphicsPipeline failed: " + SDL_GetError().getString(0));
			SDL_ReleaseGPUShader(gpuDevice, vertShader);
			SDL_ReleaseGPUShader(gpuDevice, fragShader);
			//------
			final int vertBufLen = Math.toIntExact(JAVA_FLOAT.byteSize() * 9);
			final var vboCreateInfo = SDL_GPUBufferCreateInfo.allocate(arena);
			SDL_GPUBufferCreateInfo.usage(vboCreateInfo, SDL_GPU_BUFFERUSAGE_VERTEX());
			SDL_GPUBufferCreateInfo.size(vboCreateInfo, vertBufLen);
			vertBuf = SDL_CreateGPUBuffer(gpuDevice, vboCreateInfo);
			if (vertBuf == NULL)
				throw new RuntimeException("SDL_CreateGPUBuffer failed: " + SDL_GetError().getString(0));
			final var tboCreateInfo = SDL_GPUTransferBufferCreateInfo.allocate(arena);
			SDL_GPUTransferBufferCreateInfo.usage(tboCreateInfo, SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD());
			SDL_GPUTransferBufferCreateInfo.size(tboCreateInfo, vertBufLen);
			final var tranBuf = SDL_CreateGPUTransferBuffer(gpuDevice, tboCreateInfo);
			if (tranBuf == NULL)
				throw new RuntimeException("SDL_CreateGPUTransferBuffer failed: " + SDL_GetError().getString(0));
			final var tranData = SDL_MapGPUTransferBuffer(gpuDevice, tranBuf, false);
			if (tranData == NULL)
				throw new RuntimeException("SDL_MapGPUTransferBuffer failed: " + SDL_GetError().getString(0));
			MemorySegment.copy(new float[]{
					-0.5f, -0.5f, 0.0f,
					0.5f, -0.5f, 0.0f,
					0.0f, 0.5f, 0.0f,
			}, 0, tranData, JAVA_FLOAT, 0, 9);
			SDL_UnmapGPUTransferBuffer(gpuDevice, tranBuf);
			final var uploadCmdBuf = SDL_AcquireGPUCommandBuffer(gpuDevice);
			if (uploadCmdBuf == NULL)
				throw new RuntimeException("SDL_AcquireGPUCommandBuffer failed: " + SDL_GetError().getString(0));
			final var copyPass = SDL_BeginGPUCopyPass(uploadCmdBuf);
			if (copyPass == NULL)
				throw new RuntimeException("SDL_BeginGPUCopyPass failed: " + SDL_GetError().getString(0));
			final var tranBufLoc = SDL_GPUTransferBufferLocation.allocate(arena);
			SDL_GPUTransferBufferLocation.transfer_buffer(tranBufLoc, tranBuf);
			SDL_GPUTransferBufferLocation.offset(tranBufLoc, 0);
			final var bufRegion = SDL_GPUBufferRegion.allocate(arena);
			SDL_GPUBufferRegion.buffer(bufRegion, vertBuf);
			SDL_GPUBufferRegion.offset(bufRegion, 0);
			SDL_GPUBufferRegion.size(bufRegion, vertBufLen);
			SDL_UploadToGPUBuffer(copyPass, tranBufLoc, bufRegion, false);
			SDL_EndGPUCopyPass(copyPass);
			if (!SDL_SubmitGPUCommandBuffer(uploadCmdBuf))
				throw new RuntimeException("SDL_SubmitGPUCommandBuffer failed: " + SDL_GetError().getString(0));
			SDL_ReleaseGPUTransferBuffer(gpuDevice, tranBuf);
			final var bufBinding = SDL_GPUBufferBinding.allocate(arena);

//			var renderDriver = NULL;
//			for (int i = 0, n = SDL_GetNumRenderDrivers(); i < n; i++) {
//				final var rd = SDL_GetRenderDriver(i);
//				final var rds = rd.getString(0);
//				System.out.println("RenderDriver-" + i + ": " + rds);
//				if (rds.equals("vulkan"))
//					renderDriver = rd;
//			}
//			renderer = SDL_CreateRenderer(window, renderDriver);
//			if (renderer == NULL)
//				throw new RuntimeException("SDL_CreateRenderer failed: " + SDL_GetError().getString(0));
//			SDL_SetRenderVSync(renderer, 1);
//			SDL_ShowWindow(window);
//			final int numberOfPoints = 500;
//			final var points = SDL_FPoint.allocateArray(numberOfPoints, arena);
//			for (int i = 0; i < numberOfPoints; i++) {
//				final var point = SDL_FPoint.asSlice(points, i);
//				SDL_FPoint.x(point, SDL_randf() * 440 + 100);
//				SDL_FPoint.y(point, SDL_randf() * 280 + 100);
//			}
//			final var rect = SDL_FRect.allocate(arena);

			final var event = SDL_Event.allocate(arena);
			final var swapTexPtr = arena.allocate(ADDRESS, 1);
			final var stWidthPtr = arena.allocate(JAVA_INT, 1);
			final var stHeightPtr = arena.allocate(JAVA_INT, 1);
			final var colorTargetInfo = SDL_GPUColorTargetInfo.allocate(arena);
			final var clearColor = SDL_FColor.allocate(arena);
			SDL_FColor.r(clearColor, 0.16f);
			SDL_FColor.g(clearColor, 0.47f);
			SDL_FColor.b(clearColor, 0.34f);
			SDL_FColor.a(clearColor, 1.0f);
			appLoop:
			for (; ; ) {
				while (SDL_PollEvent(event)) {
					final int eventType = SDL_Event.type(event);
					if (eventType == SDL_EVENT_KEY_DOWN()) {
						if (SDL_KeyboardEvent.key(SDL_Event.key(event)) == SDLK_ESCAPE())
							break appLoop;
					} else if (eventType == SDL_EVENT_QUIT())
						break appLoop;
				}

				final var cmdBuf = SDL_AcquireGPUCommandBuffer(gpuDevice);
				if (cmdBuf == NULL)
					throw new RuntimeException("SDL_AcquireGPUCommandBuffer failed: " + SDL_GetError().getString(0));
				if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmdBuf, window, swapTexPtr, stWidthPtr, stHeightPtr))
					throw new RuntimeException("SDL_WaitAndAcquireGPUSwapchainTexture failed: " + SDL_GetError().getString(0));
				final var swapTex = swapTexPtr.get(ADDRESS, 0);
				if (swapTex != NULL) {
					SDL_GPUColorTargetInfo.texture(colorTargetInfo, swapTex);
					SDL_GPUColorTargetInfo.mip_level(colorTargetInfo, 0);
					SDL_GPUColorTargetInfo.layer_or_depth_plane(colorTargetInfo, 0);
					SDL_GPUColorTargetInfo.clear_color(colorTargetInfo, clearColor);
					SDL_GPUColorTargetInfo.load_op(colorTargetInfo, SDL_GPU_LOADOP_CLEAR());
					SDL_GPUColorTargetInfo.store_op(colorTargetInfo, SDL_GPU_STOREOP_STORE());
					SDL_GPUColorTargetInfo.resolve_texture(colorTargetInfo, NULL);
					SDL_GPUColorTargetInfo.resolve_mip_level(colorTargetInfo, 0);
					SDL_GPUColorTargetInfo.resolve_layer(colorTargetInfo, 0);
					SDL_GPUColorTargetInfo.cycle(colorTargetInfo, true);
					SDL_GPUColorTargetInfo.cycle_resolve_texture(colorTargetInfo, false);
					final var renderPass = SDL_BeginGPURenderPass(cmdBuf, colorTargetInfo, 1, NULL);
					if (renderPass == NULL)
						throw new RuntimeException("SDL_BeginGPURenderPass failed: " + SDL_GetError().getString(0));
					SDL_BindGPUGraphicsPipeline(renderPass, pipeline);
					SDL_GPUBufferBinding.buffer(bufBinding, vertBuf);
					SDL_GPUBufferBinding.offset(bufBinding, 0);
					SDL_BindGPUVertexBuffers(renderPass, 0, bufBinding, 1);
					SDL_DrawGPUPrimitives(renderPass, 3, 1, 0, 0);
					SDL_EndGPURenderPass(renderPass); // can NOT use renderPass below
				}
				if (!SDL_SubmitGPUCommandBuffer(cmdBuf)) // can NOT use cmdBuf below
					throw new RuntimeException("SDL_SubmitGPUCommandBuffer failed: " + SDL_GetError().getString(0));

//				SDL_SetRenderDrawColor(renderer, (byte)33, (byte)33, (byte)33, (byte)255); // dark gray
//				SDL_RenderClear(renderer);
//
//				// draw a filled rectangle in the middle of the canvas
//				SDL_SetRenderDrawColor(renderer, (byte)0, (byte)0, (byte)255, (byte)255); // blue
//				SDL_FRect.x(rect, 100);
//				SDL_FRect.y(rect, 100);
//				SDL_FRect.w(rect, 440);
//				SDL_FRect.h(rect, 280);
//				SDL_RenderFillRect(renderer, rect);
//
//				// draw some points across the canvas
//				SDL_SetRenderDrawColor(renderer, (byte)255, (byte)0, (byte)0, (byte)255); // red
//				SDL_RenderPoints(renderer, points, numberOfPoints);
//
//				// draw an unfilled rectangle in-set a little bit
//				SDL_SetRenderDrawColor(renderer, (byte)0, (byte)255, (byte)0, (byte)255);  // green
//				SDL_FRect.x(rect, SDL_FRect.x(rect) + 30);
//				SDL_FRect.y(rect, SDL_FRect.y(rect) + 30);
//				SDL_FRect.w(rect, SDL_FRect.w(rect) - 60);
//				SDL_FRect.h(rect, SDL_FRect.h(rect) - 60);
//				SDL_RenderRect(renderer, rect);
//
//				// draw two lines in an X across the whole canvas
//				SDL_SetRenderDrawColor(renderer, (byte)255, (byte)255, (byte)0, (byte)255); // yellow
//				SDL_RenderLine(renderer, 0, 0, 640, 480);
//				SDL_RenderLine(renderer, 0, 480, 640, 0);
//
//				SDL_RenderPresent(renderer);
			}
		} finally {
			if (gpuDevice != NULL) {
				if (vertBuf != NULL)
					SDL_ReleaseGPUBuffer(gpuDevice, vertBuf);
				if (pipeline != NULL)
					SDL_ReleaseGPUGraphicsPipeline(gpuDevice, pipeline);
				if (window != NULL)
					SDL_ReleaseWindowFromGPUDevice(gpuDevice, window);
				SDL_DestroyGPUDevice(gpuDevice);
			}
//			if (renderer != NULL)
//				SDL_DestroyRenderer(renderer);
			if (window != NULL)
				SDL_DestroyWindow(window);
			if (audioStream != NULL)
				SDL_DestroyAudioStream(audioStream);
			if (audioDeviceId != 0)
				SDL_CloseAudioDevice(audioDeviceId);
			if (inited)
				SDL_Quit();
			System.out.println("END");
		}
	}
}
